<!DOCTYPE html>
<html>
<!--
Copyright 2014 The Closure Library Authors. All Rights Reserved.

Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
  <title>Closure Unit Tests - goog.pubsub.TypedPubSub</title>
  <script src="../base.js"></script>
  <script>
    goog.require('goog.array');
    goog.require('goog.pubsub.TopicId');
    goog.require('goog.pubsub.TypedPubSub');
    goog.require('goog.testing.jsunit');
  </script>
</head>
<body>
  <script>
    var pubsub;

    function setUp() {
      pubsub = new goog.pubsub.TypedPubSub();
    }

    function tearDown() {
      pubsub.dispose();
    }

    function testConstructor() {
      assertNotNull('PubSub instance must not be null', pubsub);
      assertTrue('PubSub instance must have the expected type',
          pubsub instanceof goog.pubsub.TypedPubSub);
    }

    function testDispose() {
      assertFalse('PubSub instance must not have been disposed of',
          pubsub.isDisposed());
      pubsub.dispose();
      assertTrue('PubSub instance must have been disposed of',
          pubsub.isDisposed());
    }

    function testSubscribeUnsubscribe() {
      function foo1() {
      }
      function bar1() {
      }
      function foo2() {
      }
      function bar2() {
      }

      /** const */ var FOO = new goog.pubsub.TopicId('foo');
      /** const */ var BAR = new goog.pubsub.TopicId('bar');
      /** const */ var BAZ = new goog.pubsub.TopicId('baz');

      assertEquals('Topic "foo" must not have any subscribers', 0,
          pubsub.getCount(FOO));
      assertEquals('Topic "bar" must not have any subscribers', 0,
          pubsub.getCount(BAR));

      pubsub.subscribe(FOO, foo1);
      assertEquals('Topic "foo" must have 1 subscriber', 1,
          pubsub.getCount(FOO));
      assertEquals('Topic "bar" must not have any subscribers', 0,
          pubsub.getCount(BAR));

      pubsub.subscribe(BAR, bar1);
      assertEquals('Topic "foo" must have 1 subscriber', 1,
          pubsub.getCount(FOO));
      assertEquals('Topic "bar" must have 1 subscriber', 1,
          pubsub.getCount(BAR));

      pubsub.subscribe(FOO, foo2);
      assertEquals('Topic "foo" must have 2 subscribers', 2,
          pubsub.getCount(FOO));
      assertEquals('Topic "bar" must have 1 subscriber', 1,
          pubsub.getCount(BAR));

      pubsub.subscribe(BAR, bar2);
      assertEquals('Topic "foo" must have 2 subscribers', 2,
          pubsub.getCount(FOO));
      assertEquals('Topic "bar" must have 2 subscribers', 2,
          pubsub.getCount(BAR));

      assertTrue(pubsub.unsubscribe(FOO, foo1));
      assertEquals('Topic "foo" must have 1 subscriber', 1,
          pubsub.getCount(FOO));
      assertEquals('Topic "bar" must have 2 subscribers', 2,
          pubsub.getCount(BAR));

      assertTrue(pubsub.unsubscribe(FOO, foo2));
      assertEquals('Topic "foo" must have no subscribers', 0,
          pubsub.getCount(FOO));
      assertEquals('Topic "bar" must have 2 subscribers', 2,
          pubsub.getCount(BAR));

      assertTrue(pubsub.unsubscribe(BAR, bar1));
      assertEquals('Topic "foo" must have no subscribers', 0,
          pubsub.getCount(FOO));
      assertEquals('Topic "bar" must have 1 subscriber', 1,
          pubsub.getCount(BAR));

      assertTrue(pubsub.unsubscribe(BAR, bar2));
      assertEquals('Topic "foo" must have no subscribers', 0,
          pubsub.getCount(FOO));
      assertEquals('Topic "bar" must have no subscribers', 0,
          pubsub.getCount(BAR));

      assertFalse('Unsubscribing a nonexistent topic must return false',
          pubsub.unsubscribe(BAZ, foo1));

      assertFalse('Unsubscribing a nonexistent function must return false',
          pubsub.unsubscribe(FOO, function() {}));
    }

    function testSubscribeUnsubscribeWithContext() {
      function foo() {
      }
      function bar() {
      }

      var contextA = {};
      var contextB = {};

      /** const */ var TOPIC_X = new goog.pubsub.TopicId('X');

      assertEquals('Topic "X" must not have any subscribers', 0,
          pubsub.getCount(TOPIC_X));

      pubsub.subscribe(TOPIC_X, foo, contextA);
      assertEquals('Topic "X" must have 1 subscriber', 1,
          pubsub.getCount(TOPIC_X));

      pubsub.subscribe(TOPIC_X, bar);
      assertEquals('Topic "X" must have 2 subscribers', 2,
          pubsub.getCount(TOPIC_X));

      pubsub.subscribe(TOPIC_X, bar, contextB);
      assertEquals('Topic "X" must have 3 subscribers', 3,
          pubsub.getCount(TOPIC_X));

      assertFalse('Unknown function/context combination return false',
          pubsub.unsubscribe(TOPIC_X, foo, contextB));

      assertTrue(pubsub.unsubscribe(TOPIC_X, foo, contextA));
      assertEquals('Topic "X" must have 2 subscribers', 2,
          pubsub.getCount(TOPIC_X));

      assertTrue(pubsub.unsubscribe(TOPIC_X, bar));
      assertEquals('Topic "X" must have 1 subscriber', 1,
          pubsub.getCount(TOPIC_X));

      assertTrue(pubsub.unsubscribe(TOPIC_X, bar, contextB));
      assertEquals('Topic "X" must have no subscribers', 0,
          pubsub.getCount(TOPIC_X));
    }

    function testSubscribeOnce() {
      var called, context;

      /** @const */ SOME_TOPIC = new goog.pubsub.TopicId('someTopic');

      called = false;
      pubsub.subscribeOnce(SOME_TOPIC, function() {
        called = true;
      });
      assertEquals('Topic must have one subscriber', 1,
          pubsub.getCount(SOME_TOPIC));
      assertFalse('Subscriber must not have been called yet', called);

      pubsub.publish(SOME_TOPIC);
      assertEquals('Topic must have no subscribers', 0,
          pubsub.getCount(SOME_TOPIC));
      assertTrue('Subscriber must have been called', called);

      context = {called: false};
      pubsub.subscribeOnce(SOME_TOPIC, function() {
        this.called = true;
      }, context);
      assertEquals('Topic must have one subscriber', 1,
          pubsub.getCount(SOME_TOPIC));
      assertFalse('Subscriber must not have been called yet', context.called);

      pubsub.publish(SOME_TOPIC);
      assertEquals('Topic must have no subscribers', 0,
          pubsub.getCount(SOME_TOPIC));
      assertTrue('Subscriber must have been called', context.called);

      context = {called: false, value: 0};
      pubsub.subscribeOnce(SOME_TOPIC, function(value) {
        this.called = true;
        this.value = value;
      }, context);
      assertEquals('Topic must have one subscriber', 1,
          pubsub.getCount(SOME_TOPIC));
      assertFalse('Subscriber must not have been called yet', context.called);
      assertEquals('Value must have expected value', 0, context.value);

      pubsub.publish(SOME_TOPIC, 17);
      assertEquals('Topic must have no subscribers', 0,
          pubsub.getCount(SOME_TOPIC));
      assertTrue('Subscriber must have been called', context.called);
      assertEquals('Value must have been updated', 17, context.value);
    }

    function testSubscribeOnce_boundFn() {
      var context = {called: false, value: 0};

      /** @const */ SOME_TOPIC = new goog.pubsub.TopicId('someTopic');

      function subscriber(value) {
        this.called = true;
        this.value = value;
      }

      pubsub.subscribeOnce(SOME_TOPIC, goog.bind(subscriber, context));
      assertEquals('Topic must have one subscriber', 1,
          pubsub.getCount(SOME_TOPIC));
      assertFalse('Subscriber must not have been called yet', context.called);
      assertEquals('Value must have expected value', 0, context.value);

      pubsub.publish(SOME_TOPIC, 17);
      assertEquals('Topic must have no subscribers', 0,
          pubsub.getCount(SOME_TOPIC));
      assertTrue('Subscriber must have been called', context.called);
      assertEquals('Value must have been updated', 17, context.value);
    }

    function testSubscribeOnce_partialFn() {
      var called = false;
      var value = 0;

      /** @const */ SOME_TOPIC = new goog.pubsub.TopicId('someTopic');

      function subscriber(hasBeenCalled, newValue) {
        called = hasBeenCalled;
        value = newValue;
      }

      pubsub.subscribeOnce(SOME_TOPIC, goog.partial(subscriber, true));
      assertEquals('Topic must have one subscriber', 1,
          pubsub.getCount(SOME_TOPIC));
      assertFalse('Subscriber must not have been called yet', called);
      assertEquals('Value must have expected value', 0, value);

      pubsub.publish(SOME_TOPIC, 17);
      assertEquals('Topic must have no subscribers', 0,
          pubsub.getCount(SOME_TOPIC));
      assertTrue('Subscriber must have been called', called);
      assertEquals('Value must have been updated', 17, value);
    }

    function testSelfResubscribe() {
      var value = null;

      /** @const */ SOME_TOPIC = new goog.pubsub.TopicId('someTopic');

      function resubscribe(iteration, newValue) {
        pubsub.subscribeOnce(SOME_TOPIC,
            goog.partial(resubscribe, iteration + 1));
        value = newValue + ':' + iteration;
      }

      pubsub.subscribeOnce(SOME_TOPIC, goog.partial(resubscribe, 0));
      assertEquals('Topic must have 1 subscriber', 1,
          pubsub.getCount(SOME_TOPIC));
      assertNull('Value must be null', value);

      pubsub.publish(SOME_TOPIC, 'foo');
      assertEquals('Topic must have 1 subscriber', 1,
          pubsub.getCount(SOME_TOPIC));
      assertEquals('Value be as expected', 'foo:0', value);

      pubsub.publish(SOME_TOPIC, 'bar');
      assertEquals('Topic must have 1 subscriber', 1,
          pubsub.getCount(SOME_TOPIC));
      assertEquals('Value be as expected', 'bar:1', value);

      pubsub.publish(SOME_TOPIC, 'baz');
      assertEquals('Topic must have 1 subscriber', 1,
          pubsub.getCount(SOME_TOPIC));
      assertEquals('Value be as expected', 'baz:2', value);
    }

    function testUnsubscribeByKey() {
      var key1, key2, key3;

      /** const */ var TOPIC_X = new goog.pubsub.TopicId('X');
      /** const */ var TOPIC_Y = new goog.pubsub.TopicId('Y');

      key1 = pubsub.subscribe(TOPIC_X, function() {});
      key2 = pubsub.subscribe(TOPIC_Y, function() {});

      assertEquals('Topic "X" must have 1 subscriber', 1,
          pubsub.getCount(TOPIC_X));
      assertEquals('Topic "Y" must have 1 subscriber', 1,
          pubsub.getCount(TOPIC_Y));
      assertNotEquals('Subscription keys must be distinct', key1, key2);

      pubsub.unsubscribeByKey(key1);
      assertEquals('Topic "X" must have no subscribers', 0,
          pubsub.getCount(TOPIC_X));
      assertEquals('Topic "Y" must have 1 subscriber', 1,
          pubsub.getCount(TOPIC_Y));

      key3 = pubsub.subscribe(TOPIC_X, function() {});
      assertEquals('Topic "X" must have 1 subscriber', 1,
          pubsub.getCount(TOPIC_X));
      assertEquals('Topic "Y" must have 1 subscriber', 1,
          pubsub.getCount(TOPIC_Y));
      assertNotEquals('Subscription keys must be distinct', key1, key3);
      assertNotEquals('Subscription keys must be distinct', key2, key3);

      pubsub.unsubscribeByKey(key1); // Obsolete key; should be no-op.
      assertEquals('Topic "X" must have 1 subscriber', 1,
          pubsub.getCount(TOPIC_X));
      assertEquals('Topic "Y" must have 1 subscriber', 1,
          pubsub.getCount(TOPIC_Y));

      pubsub.unsubscribeByKey(key2);
      assertEquals('Topic "X" must have 1 subscriber', 1,
          pubsub.getCount(TOPIC_X));
      assertEquals('Topic "Y" must have no subscribers', 0,
          pubsub.getCount(TOPIC_Y));

      pubsub.unsubscribeByKey(key3);
      assertEquals('Topic "X" must have no subscribers', 0,
          pubsub.getCount(TOPIC_X));
      assertEquals('Topic "Y" must have no subscribers', 0,
          pubsub.getCount(TOPIC_Y));
    }

    function testSubscribeUnsubscribeMultiple() {
      function foo() {
      }
      function bar() {
      }

      var context = {};

      /** const */ var TOPIC_X = new goog.pubsub.TopicId('X');
      /** const */ var TOPIC_Y = new goog.pubsub.TopicId('Y');
      /** const */ var TOPIC_Z = new goog.pubsub.TopicId('Z');

      assertEquals('Pubsub channel must not have any subscribers', 0,
          pubsub.getCount());

      assertEquals('Topic "X" must not have any subscribers', 0,
          pubsub.getCount(TOPIC_X));
      assertEquals('Topic "Y" must not have any subscribers', 0,
          pubsub.getCount(TOPIC_Y));
      assertEquals('Topic "Z" must not have any subscribers', 0,
          pubsub.getCount(TOPIC_Z));

      goog.array.forEach([TOPIC_X, TOPIC_Y, TOPIC_Z], function(topic) {
        pubsub.subscribe(topic, foo);
      });
      assertEquals('Topic "X" must have 1 subscriber', 1,
          pubsub.getCount(TOPIC_X));
      assertEquals('Topic "Y" must have 1 subscriber', 1,
          pubsub.getCount(TOPIC_Y));
      assertEquals('Topic "Z" must have 1 subscriber', 1,
          pubsub.getCount(TOPIC_Z));

      goog.array.forEach([TOPIC_X, TOPIC_Y, TOPIC_Z], function(topic) {
        pubsub.subscribe(topic, bar, context);
      });
      assertEquals('Topic "X" must have 2 subscribers', 2,
          pubsub.getCount(TOPIC_X));
      assertEquals('Topic "Y" must have 2 subscribers', 2,
          pubsub.getCount(TOPIC_Y));
      assertEquals('Topic "Z" must have 2 subscribers', 2,
          pubsub.getCount(TOPIC_Z));

      assertEquals('Pubsub channel must have a total of 6 subscribers', 6,
          pubsub.getCount());

      goog.array.forEach([TOPIC_X, TOPIC_Y, TOPIC_Z], function(topic) {
        pubsub.unsubscribe(topic, foo);
      });
      assertEquals('Topic "X" must have 1 subscriber', 1,
          pubsub.getCount(TOPIC_X));
      assertEquals('Topic "Y" must have 1 subscriber', 1,
          pubsub.getCount(TOPIC_Y));
      assertEquals('Topic "Z" must have 1 subscriber', 1,
          pubsub.getCount(TOPIC_Z));

      goog.array.forEach([TOPIC_X, TOPIC_Y, TOPIC_Z], function(topic) {
        pubsub.unsubscribe(topic, bar, context);
      });
      assertEquals('Topic "X" must not have any subscribers', 0,
          pubsub.getCount(TOPIC_X));
      assertEquals('Topic "Y" must not have any subscribers', 0,
          pubsub.getCount(TOPIC_Y));
      assertEquals('Topic "Z" must not have any subscribers', 0,
          pubsub.getCount(TOPIC_Z));

      assertEquals('Pubsub channel must not have any subscribers', 0,
          pubsub.getCount());
    }

    function testPublish() {
      var context = {};
      var fooCalled = false;
      var barCalled = false;

      /** @const */ SOME_TOPIC = new goog.pubsub.TopicId('someTopic');

      function foo(record) {
        fooCalled = true;
        assertEquals('x must have expected value', 'x', record.x);
        assertEquals('y must have expected value', 'y', record.y);
      }

      function bar(record) {
        barCalled = true;
        assertEquals('Context must have expected value', context, this);
        assertEquals('x must have expected value', 'x', record.x);
        assertEquals('y must have expected value', 'y', record.y);
      }

      pubsub.subscribe(SOME_TOPIC, foo);
      pubsub.subscribe(SOME_TOPIC, bar, context);

      assertTrue(pubsub.publish(SOME_TOPIC, {x: 'x', y: 'y'}));
      assertTrue('foo() must have been called', fooCalled);
      assertTrue('bar() must have been called', barCalled);

      fooCalled = false;
      barCalled = false;
      assertTrue(pubsub.unsubscribe(SOME_TOPIC, foo));

      assertTrue(pubsub.publish(SOME_TOPIC, {x: 'x', y: 'y'}));
      assertFalse('foo() must not have been called', fooCalled);
      assertTrue('bar() must have been called', barCalled);

      fooCalled = false;
      barCalled = false;
      pubsub.subscribe('differentTopic', foo);

      assertTrue(pubsub.publish(SOME_TOPIC, {x: 'x', y: 'y'}));
      assertFalse('foo() must not have been called', fooCalled);
      assertTrue('bar() must have been called', barCalled);
    }

    function testPublishEmptyTopic() {
      var fooCalled = false;
      function foo() {
        fooCalled = true;
      }

      /** @const */ SOME_TOPIC = new goog.pubsub.TopicId('someTopic');

      assertFalse('Publishing to nonexistent topic must return false',
          pubsub.publish(SOME_TOPIC));

      pubsub.subscribe(SOME_TOPIC, foo);
      assertTrue('Publishing to topic with subscriber must return true',
          pubsub.publish(SOME_TOPIC));
      assertTrue('Foo must have been called', fooCalled);

      pubsub.unsubscribe(SOME_TOPIC, foo);
      fooCalled = false;
      assertFalse('Publishing to topic without subscribers must return false',
          pubsub.publish(SOME_TOPIC));
      assertFalse('Foo must nothave been called', fooCalled);
    }

    function testSubscribeWhilePublishing() {
      // It's OK for a subscriber to add a new subscriber to its own topic,
      // but the newly added subscriber shouldn't be called until the next
      // publish cycle.

      var firstCalled = false;
      var secondCalled = false;

      /** @const */ SOME_TOPIC = new goog.pubsub.TopicId('someTopic');

      pubsub.subscribe(SOME_TOPIC, function() {
        pubsub.subscribe(SOME_TOPIC, function() {
          secondCalled = true;
        });
        firstCalled = true;
      });
      assertEquals('Topic must have one subscriber', 1,
          pubsub.getCount(SOME_TOPIC));
      assertFalse('No subscriber must have been called yet',
          firstCalled || secondCalled);

      pubsub.publish(SOME_TOPIC);
      assertEquals('Topic must have two subscribers', 2,
          pubsub.getCount(SOME_TOPIC));
      assertTrue('The first subscriber must have been called',
          firstCalled);
      assertFalse('The second subscriber must not have been called yet',
          secondCalled);

      pubsub.publish(SOME_TOPIC);
      assertEquals('Topic must have three subscribers', 3,
          pubsub.getCount(SOME_TOPIC));
      assertTrue('The first subscriber must have been called',
          firstCalled);
      assertTrue('The second subscriber must also have been called',
          secondCalled);
    }

    function testUnsubscribeWhilePublishing() {
      // It's OK for a subscriber to unsubscribe another subscriber from its
      // own topic, but the subscriber in question won't actually be removed
      // until after publishing is complete.

      var firstCalled = false;
      var secondCalled = false;
      var thirdCalled = false;

      /** const */ var TOPIC_X= new goog.pubsub.TopicId('X');

      function first() {
        assertFalse('unsubscribe() must return false during publishing',
            pubsub.unsubscribe(TOPIC_X, second));
        assertEquals('Topic "X" must still have 3 subscribers', 3,
            pubsub.getCount(TOPIC_X));
        firstCalled = true;
      }
      pubsub.subscribe(TOPIC_X, first);

      function second() {
        assertEquals('Topic "X" must still have 3 subscribers', 3,
            pubsub.getCount(TOPIC_X));
        secondCalled = true;
      }
      pubsub.subscribe(TOPIC_X, second);

      function third() {
        assertFalse('unsubscribe() must return false during publishing',
            pubsub.unsubscribe(TOPIC_X, first));
        assertEquals('Topic "X" must still have 3 subscribers', 3,
            pubsub.getCount(TOPIC_X));
        thirdCalled = true;
      }
      pubsub.subscribe(TOPIC_X, third);

      assertEquals('Topic "X" must have 3 subscribers', 3,
          pubsub.getCount(TOPIC_X));
      assertFalse('No subscribers must have been called yet',
          firstCalled || secondCalled || thirdCalled);

      assertTrue(pubsub.publish(TOPIC_X));
      assertTrue('First function must have been called', firstCalled);
      assertTrue('Second function must have been called', secondCalled);
      assertTrue('Third function must have been called', thirdCalled);
      assertEquals('Topic "X" must have 1 subscriber after publishing', 1,
          pubsub.getCount(TOPIC_X));
    }

    function testUnsubscribeSelfWhilePublishing() {
      // It's OK for a subscriber to unsubscribe itself, but it won't actually
      // be removed until after publishing is complete.

      var selfDestructCalled = false;

      /** @const */ SOME_TOPIC = new goog.pubsub.TopicId('someTopic');

      function selfDestruct() {
        assertFalse('unsubscribe() must return false during publishing',
            pubsub.unsubscribe(SOME_TOPIC, arguments.callee));
        assertEquals('Topic must still have 1 subscriber', 1,
            pubsub.getCount(SOME_TOPIC));
        selfDestructCalled = true;
      }

      pubsub.subscribe(SOME_TOPIC, selfDestruct);
      assertEquals('Topic must have 1 subscriber', 1,
          pubsub.getCount(SOME_TOPIC));
      assertFalse('selfDestruct() must not have been called yet',
          selfDestructCalled);

      pubsub.publish(SOME_TOPIC);
      assertTrue('selfDestruct() must have been called', selfDestructCalled);
      assertEquals('Topic must have no subscribers after publishing', 0,
          pubsub.getCount(SOME_TOPIC));
    }

    function testPublishReturnValue() {
      /** @const */ SOME_TOPIC = new goog.pubsub.TopicId('someTopic');
      pubsub.subscribe(SOME_TOPIC, function() {
        pubsub.unsubscribe(SOME_TOPIC, arguments.callee);
      });
      assertTrue('publish() must return true even if the only subscriber ' +
          'removes itself during publishing', pubsub.publish(SOME_TOPIC));
    }

    function testNestedPublish() {
      var x1 = false;
      var x2 = false;
      var y1 = false;
      var y2 = false;

      /** @const */ TOPIC_X = new goog.pubsub.TopicId('X');
      /** @const */ TOPIC_Y = new goog.pubsub.TopicId('Y');

      pubsub.subscribe(TOPIC_X, function() {
        pubsub.publish(TOPIC_Y);
        pubsub.unsubscribe(TOPIC_X, arguments.callee);
        x1 = true;
      });

      pubsub.subscribe(TOPIC_X, function() {
        x2 = true;
      });

      pubsub.subscribe(TOPIC_Y, function() {
        pubsub.unsubscribe(TOPIC_Y, arguments.callee);
        y1 = true;
      });

      pubsub.subscribe(TOPIC_Y, function() {
        y2 = true;
      });

      pubsub.publish(TOPIC_X);

      assertTrue('x1 must be true', x1);
      assertTrue('x2 must be true', x2);
      assertTrue('y1 must be true', y1);
      assertTrue('y2 must be true', y2);
    }

    function testClear() {
      function fn() {
      }

      var topics = [
        new goog.pubsub.TopicId('W'),
        new goog.pubsub.TopicId('X'),
        new goog.pubsub.TopicId('Y'),
        new goog.pubsub.TopicId('Z'),
      ];

      goog.array.forEach(topics, function(topic) {
        pubsub.subscribe(topic, fn);
      });
      assertEquals('Pubsub channel must have 4 subscribers', 4,
          pubsub.getCount());

      pubsub.clear(topics[0]);
      assertEquals('Pubsub channel must have 3 subscribers', 3,
          pubsub.getCount());

      pubsub.clear(topics[1]);
      pubsub.clear(topics[2]);
      assertEquals('Pubsub channel must have 1 subscriber', 1,
          pubsub.getCount());

      pubsub.clear();
      assertEquals('Pubsub channel must have no subscribers', 0,
          pubsub.getCount());
    }
  </script>
</body>
</html>
